#pragma once

#include <stx/core/regex.hpp>

inline namespace stx
{
namespace fs
{
enum class Filter
{
    K_FILE,
    K_DIR,
    K_BOTH,
};

class Permissions final
{
    fs::perms perms_{};

public:
    constexpr Permissions() noexcept = default;

    // 644_i32, 777_i32, etc.
    explicit Permissions(IsIntegral auto const n)
        : Permissions(align(n, MinWidth(3), FillChar('0'), Align::K_RIGHT))
    {}

    // "644"sv, "777"sv, etc.
    explicit Permissions(std::string_view const sv_perms);

    [[nodiscard]] operator fs::perms() const noexcept; // NOLINT
};

[[nodiscard]] bool isFifo(fs::path const& fp) noexcept;
[[nodiscard]] bool isOther(fs::path const& fp) noexcept;
[[nodiscard]] bool isSocket(fs::path const& fp) noexcept;
[[nodiscard]] bool isBlockFile(fs::path const& fp) noexcept;
[[nodiscard]] bool isCharacterFile(fs::path const& fp) noexcept;
[[nodiscard]] bool isDirectory(fs::path const& fp) noexcept;
[[nodiscard]] bool isEmpty(fs::path const& fp) noexcept;
[[nodiscard]] bool isRegularFile(fs::path const& fp) noexcept;
[[nodiscard]] bool isSymlink(fs::path const& fp) noexcept;
[[nodiscard]] bool exists(fs::path const& fp) noexcept;

[[nodiscard]] fs::path getCurrentPath() noexcept;
[[nodiscard]] fs::path getCanonicalPath(fs::path const& fp) noexcept;
[[nodiscard]] fs::path getWeaklyCanonicalPath(fs::path const& fp) noexcept;
[[nodiscard]] fs::path readSymlink(fs::path const& fp) noexcept;
[[nodiscard]] fs::path getTempDirPath() noexcept;
[[nodiscard]] fs::path getAbsolutePath(fs::path const& fp) noexcept;

[[nodiscard]] std::size_t    getFileSize(fs::path const& fp) noexcept;
[[nodiscard]] std::size_t    getHardLinkCount(fs::path const& fp) noexcept;
[[nodiscard]] fs::space_info getSpaceInfo(fs::path const& fp) noexcept;

[[nodiscard]] fs::perms       getFilePerms(fs::path const& fp) noexcept;
[[nodiscard]] fs::file_type   getFileType(fs::path const& fp) noexcept;
[[nodiscard]] fs::file_status getFileStatus(fs::path const& fp) noexcept;

bool resizeFile(fs::path const& fp, std::size_t const new_size) noexcept;
bool createDirectory(fs::path const& fp) noexcept;
bool createDirectories(fs::path const& fp) noexcept;
bool createSymlink(fs::path const& fp_target, fs::path const& symlink) noexcept;
bool copySymlink(fs::path const& from, fs::path const& to) noexcept;
bool setCurrentPath(fs::path const& fp) noexcept;
bool touch(fs::path const& fp) noexcept;
bool rename(fs::path const& fp_old, fs::path const& fp_new) noexcept;

[[nodiscard]] fs::space_info space(fs::path const& fp) noexcept;
/*
The file or empty directory identified by the path fp is deleted as if by the 
POSIX remove. Symlinks are not followed (symlink is removed, not its target).
*/
bool        remove(fs::path const& fp) noexcept;
std::size_t removeAll(fs::path const& fp) noexcept;
std::size_t removeSubEntries(fs::path const& fp) noexcept;

std::size_t removeFiles(fs::path const& fp_dir, std::regex const& re) noexcept;
std::size_t removeDirectories(fs::path const&   fp_dir,
                              std::regex const& re) noexcept;
std::size_t removeEntries(fs::path const&   fp_dir,
                          std::regex const& re) noexcept;

std::size_t removeFilesRecursive(fs::path const&   fp_dir,
                                 std::regex const& re) noexcept;
std::size_t removeDirectoriesRecursive(fs::path const&   fp_dir,
                                       std::regex const& re) noexcept;
std::size_t removeEntriesRecursive(fs::path const&   fp_dir,
                                   std::regex const& re) noexcept;

[[nodiscard]] bool equivalent(fs::path const& fp1,
                              fs::path const& fp2) noexcept;

bool createDirectory(fs::path const& fp_new_dir,
                     fs::path const& fp_existing_dir_to_copy_perms) noexcept;

bool createDirectorySymlink(fs::path const& fp_target,
                            fs::path const& fp_symlink) noexcept;

bool createHardLink(fs::path const& fp_target,
                    fs::path const& fp_hard_link) noexcept;

bool copy(fs::path const&        fp_from,
          fs::path const&        fp_to,
          fs::copy_options const opts = fs::copy_options::none) noexcept;

bool copyFile(fs::path const&        fp_from,
              fs::path const&        fp_to,
              fs::copy_options const opts = fs::copy_options::none) noexcept;

bool setLastWriteTime(fs::path const& fp, SysTimePoint const tp_sys) noexcept;

[[nodiscard]] SysTimePoint getLastWriteTime(fs::path const& fp) noexcept;

[[nodiscard]] fs::path
getRelativePath(fs::path const& fp_given,
                fs::path const& fp_base = getCurrentPath()) noexcept;

[[nodiscard]] fs::path
getProximatePath(fs::path const& fp_given,
                 fs::path const& fp_base = getCurrentPath()) noexcept;

bool setPermissions(
    fs::path const&        fp,
    fs::perms const        pms,
    fs::perm_options const opts = fs::perm_options::replace) noexcept;

bool setPermissionsRecursive(
    fs::path const&        fp,
    fs::perms const        pms,
    fs::perm_options const opts = fs::perm_options::replace) noexcept;

[[nodiscard]] inline auto getEntryRange(fs::path const& fp)
    -> decltype(ranges::subrange(fs::directory_iterator(fp),
                                 fs::directory_iterator{}))
{
    if (!fs::exists(fp))
    {
        RUNTIME_ERROR(fp, " doesn't exists."sv);
    }

    if (!fs::isDirectory(fp))
    {
        RUNTIME_ERROR(fp, " is not a directory."sv);
    }

    return ranges::subrange(std::fs::directory_iterator(fp),
                            std::fs::directory_iterator{});
}

[[nodiscard]] inline auto getFileRange(fs::path const& fp)
{
    return getEntryRange(fp) |
           views::filter([](auto const& entry)
                         { return entry.is_regular_file(); });
}

[[nodiscard]] inline auto getDirRange(fs::path const& fp)
{
    return getEntryRange(fp) | views::filter([](auto const& entry)
                                             { return entry.is_directory(); });
}

[[nodiscard]] inline auto getEntryRange(fs::path const&   fp,
                                        std::regex const& re)
{
    return getEntryRange(fp) |
           views::filter([&re](auto const& entry)
                         { return isMatch(entry.path().native(), re); });
}

[[nodiscard]] inline auto getFileRange(fs::path const& fp, std::regex const& re)
{
    return getEntryRange(fp) |
           views::filter(
               [&re](auto const& entry) {
                   return entry.is_regular_file() &&
                          isMatch(entry.path().native(), re);
               });
}

[[nodiscard]] inline auto getDirRange(fs::path const& fp, std::regex const& re)
{
    return getEntryRange(fp) |
           views::filter(
               [&re](auto const& entry) {
                   return entry.is_directory() &&
                          isMatch(entry.path().native(), re);
               });
}

[[nodiscard]] inline auto getEntryRangeRecursive(fs::path const& fp)
    -> decltype(ranges::subrange(fs::recursive_directory_iterator(fp),
                                 fs::recursive_directory_iterator{}))
{
    if (!fs::exists(fp))
    {
        RUNTIME_ERROR(fp, " doesn't exists."sv);
    }

    if (!fs::isDirectory(fp))
    {
        RUNTIME_ERROR(fp, " is not a directory."sv);
    }

    return ranges::subrange(std::fs::recursive_directory_iterator(fp),
                            std::fs::recursive_directory_iterator{});
}

[[nodiscard]] inline auto getFileRangeRecursive(fs::path const& fp)
{
    return getEntryRangeRecursive(fp) |
           views::filter([](auto const& entry)
                         { return entry.is_regular_file(); });
}

[[nodiscard]] inline auto getDirRangeRecursive(fs::path const& fp)
{
    return getEntryRangeRecursive(fp) |
           views::filter([](auto const& entry)
                         { return entry.is_directory(); });
}

[[nodiscard]] inline auto getEntryRangeRecursive(fs::path const&   fp,
                                                 std::regex const& re)
{
    return getEntryRangeRecursive(fp) |
           views::filter([&re](auto const& entry)
                         { return isMatch(entry.path().native(), re); });
}

[[nodiscard]] inline auto getFileRangeRecursive(fs::path const&   fp,
                                                std::regex const& re)
{
    return getEntryRangeRecursive(fp) |
           views::filter(
               [&re](auto const& entry) {
                   return entry.is_regular_file() &&
                          isMatch(entry.path().native(), re);
               });
}

[[nodiscard]] inline auto getDirRangeRecursive(fs::path const&   fp,
                                               std::regex const& re)
{
    return getEntryRangeRecursive(fp) |
           views::filter(
               [&re](auto const& entry) {
                   return entry.is_directory() &&
                          isMatch(entry.path().native(), re);
               });
}
} // namespace fs
} // namespace stx

/*
fs::path::generic_string() vs fs::path::string()
------------------------------------------------

On Windows:
    Native format has backslashes and generic format has slashes.

On Linux:
    Both formats have slashes.

-------------------------------------
::mkfifo("/tmp/myfifo", S_IRWXU);
ASSERT(fs::isFifo("/tmp/myfifo"_fp));
fs::remove("/tmp/myfifo");

How to find a socket file?
--------------------------
cat /proc/net/unix
cat /proc/net/tcp
cat /proc/net/udp
cat /proc/net/raw

How to remove a socket file like "/tmp/xxx="?
---------------------------------------------
sudo rm xxx  # correct
sudo rm xxx= # incorrect

What does isOther() mean?
-------------------------
isOther(s) == exists(s) && !isRegularFile(s) && !isDirectory(s) && !isSymlink(s)

Filesystem Semantics
--------------------
On a POSIX-style filesystem, each directory has at least 2 hard links:
Itself and the special member pathname "."

Each ".." is a hard link to the parent directory, so the total number
of hard links for any directory is 2 plus number of direct subdirectories.

Each dot-dot is a hard link to parent.

The getCanonicalPath path is always absolute and unique, the function removes 
the '.' '..' from the path, if present.

The canonical path must exist. The weakly canonical path may not exist.
*/